使用 ViewBinding 混淆后 APP 炸了?

2022-08-19

1. 背景:

最近在给新项目配置混淆时,遇到了一个崩溃的问题,一打开某个 Activity 就崩溃,特此记录一下分析的过程。

crash 关键堆栈信息如下:

1
2
3
Caused by: java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType
at com.ecovacs.proguardtest.BaseBindingActivity.onCreate(BaseBindingActivity.kt:16)
at com.ecovacs.proguardtest.MainActivity.onCreate(MainActivity.kt:11)

项目构建基于 AGP 7.2.0,Gradle 7.3.3,开启了 R8 fullMode。

2. 分析:

崩溃发生的位置在 BaseBindingActivity 的 onCreate 中,这里的 BaseBindingActivity 是一个基类 Activity, 主要作用是通过泛型+反射的方式对 ViewBinding 进行封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
open class BaseBindingActivity<VB : ViewBinding> : FragmentActivity() {

protected lateinit var binding: VB

@Suppress("UNCHECKED_CAST")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val type = javaClass.genericSuperclass as ParameterizedType
val clazz = type.actualTypeArguments[0] as Class<VB>
val method = clazz.getMethod("inflate", LayoutInflater::class.java)
binding = method.invoke(null, layoutInflater) as VB
setContentView(binding.root)
}
}

这样做的好处是,在使用的时候只需指定具体的泛型类型即可获取 ViewBinding 的实例,避免了写大量重复的模版代码:

1
2
3
4
5
6
7
@AndroidEntryPoint
class MainActivity : BaseBindingActivity<ActivityMainBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
}
}

通过上面的堆栈信息,我们知道崩溃的位置发生在这一行:

1
val type = javaClass.genericSuperclass as ParameterizedType

getGenericSuperclass() 获取到的是一个 Class,不是 ParameterizedType,于是查看一下 getGenericSuperclass() 方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Type getGenericSuperclass() {
Type genericSuperclass = getSuperclass();
// This method is specified to return null for all cases where getSuperclass
// returns null, i.e, for primitives, interfaces, void and java.lang.Object.
if (genericSuperclass == null) {
return null;
}

String annotationSignature = getSignatureAttribute();
if (annotationSignature != null) {
GenericSignatureParser parser = new GenericSignatureParser(getClassLoader());
parser.parseForClass(this, annotationSignature);
genericSuperclass = parser.superclassType;
}
return Types.getType(genericSuperclass);
}

分析可能是由于没有拿到 Class 的 Signature 信息,所以直接返回了父类的 Class。可是我们的混淆文件中雀食已经配置了 -keepattributes Signature ,并且排除了混淆文件没生效的情况,因为包名类名已经被混淆的🐎都不认识了,这™就很玄学。为了再次确认是因为 Signature 信息丢失导致的,我将未混淆和混淆的两个包使用 jadx 反编译对比一下 smali 代码:

20220819172636926

可以发现混淆后的类中的 Signature 信息雀食丢了!

既然排除了项目配置的问题,那…有没有可能是 AGP 或者 R8 的 bug 呀?毕竟混淆相关的 task 都在 AGP 中呢?而且之前另一个项目中使用的是 AGP 4.2.2,也开启了 R8 fullMode,完全没问题。于是尝试把 AGP 回退到 4.2.2 版本,果然好了!我透,垃圾 Google,浪费我青春!

回退版本是不可能的,历史的车轮滚滚向前,怎么能倒退呢!况且还有一些 AGP 的插件已经适配 7.x 的 api 了!那怎么办呢,于是上 issuetracker 搜了一番,发现有人提了一个类似的问题:

R8 FullMode in AGP 7.0.0-beta01 transforming ParameterizedType implementations to Class

看了他的描述:开了 R8 fullMode,genericSuperclass 是 Class<Object>,关了 R8 fullMode 或者关闭 R8,genericSuperclass 是 ParameterizedType。这™不是和我遇到的情况一样吗?不过他的 AGP 版本是 7.0.0-beta01,但是 google 官方人员在下面回复了: Status: Won’t Fix (Intended Behavior) 。那么我们用的 AGP 7.2.0 是不是也是这个问题呢?咱也关了 fullMode 试试。试试就逝世!没啥卵用,还是同样的错误。

没办法,只好 debug 分析一下:

1660898715385

看到这里才恍然大悟,虽然 Signature 信息丢失了,直接返回了 MainActivity 父类的 Class,但是这个 Class 并不是 BaseBindingActivity,而是一个 Hilt_MainActivity。由于项目中也使用到了 Hilt,并且在 MainActivity 上加了 AndroidEntryPoint 的注解,所以 Hilt 在注解处理阶段会自动生成 Hilt_MainActivity 这个类。那么 Signature 丢失有没有可能是 Hilt 导致的呢?

This annotation will generate a base class that the annotated class should extend, either directly or via the Hilt Gradle Plugin. This base class will take care of injecting members into the Android class as well as handling instantiating the proper Hilt components at the right point in the lifecycle. The name of the base class will be “Hilt_

再逛逛 issuetracker,又发现了有个人提的一个 issue,这个 issue 和我遇到的不能说一样,简直就是完全一样!

R8 erase generic type on Activity cause reflect cast exception in fullMode

看完这个 issue 的所有评论,至此,其实问题已经解决了,是 hilt 的 bug!详情见:

[Hilt] Incorrect signature attribute?

这个问题在 hilt 2.41 版本被修复了:

Fix an issue where Hilt transform was not correctly updating the Signature attribute of an @AndroidEntryPoint whose superclass contained a type variable.

我们知道了 Hlit 在注解处理阶段会给被 AndroidEntryPoint 注解的类生成对应的 Hilt_ 类,然后 Hilt 的 Plugin 会在编译阶段修改相关类的字节码,将被 AndroidEntryPoint 注解的类的基类改为 Hilt_ ,但是有 bug 的版本在 visit 被 AndroidEntryPoint 注解的类的时候,没有更新类的 Signature 信息,从而导致 Signatue 信息丢失。具体可以点击这里查看相关改动。

我们使用的是 hilt 2.40.1,正是有问题的版本!升级 hilt 版本,搞定!下班!

3. R8 full mode

Q: Will R8 strip the signature even if I have -keepattributes Signature set in my rules?

A: Yes, R8 in full-mode will strip all signatures for not-kept classes if using -keepattributes Signature. The only way to keep the generic signature for a class is if you keep it.

4. Proguard

如果不想使用 R8,可以关闭 minifyEnabled,直接使用 Proguard 插件。

解决问题的途中,我曾试图弃用 R8,使用 Proguard 代替,但是遇到了一些问题,并且 issue 还是开启状态,只好放弃。

5. reference

  1. https://issuetracker.google.com/issues/188703877
  2. https://issuetracker.google.com/issues/216194181
  3. https://github.com/google/dagger/issues/3094
  4. https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md
  5. https://twitter.com/AGPVersions
  6. https://github.com/Guardsquare/proguard/issues/196